1 /*
2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021
3 License:   [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License].
4 Authors: Marcelo S. N. Mancini
5 
6 	Copyright Marcelo S. N. Mancini 2018 - 2021.
7 Distributed under the CC BY-4.0 License.
8    (See accompanying file LICENSE.txt or copy at
9 	https://creativecommons.org/licenses/by/4.0/
10 */
11 module hip.font.ttf;
12 import hip.api.data.font;
13 
14 immutable dstring defaultCharset = " \náéíóúãñçabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890\\|'\"`/*-+,.;:´_=!@#$%&()[]{}~^?";
15 
16 version = HipArsdFont;
17 
18 
19 private uint nextPowerOfTwo(uint number)
20 {
21     ulong value = 1;
22     while(value < number)
23     {
24         value <<= 1;
25     }
26     return cast(uint)value;
27 }
28 
29 private int round(float f)
30 {
31     return cast(int)(f+0.5);
32 }
33 
34 class HipNullFont : HipFont
35 {
36     string path;
37     this(){}
38     this(string path, uint fontSize = 32)
39     {
40     }
41     /**
42     *   This will cause a full load of the .ttf file, image generation and GPU upload. Should only be used
43     *   If you don't care about async
44     */
45     bool loadFromMemory(in ubyte[] data){return false;}
46     bool partialLoad(in ubyte[] data, out ubyte[] rawImage){return false;}
47     bool loadTexture(ubyte[] rawImage){return false;}
48     override int getKerning(dchar current, dchar next) const{return 0;}
49     override int getKerning(const(HipFontChar)* current, const(HipFontChar)* next) const{return 0;}
50     override HipFont getFontWithSize(uint size){return new HipNullFont();}
51 }
52 
53 
54 version(HipArsdFont)
55 /**
56 *   Check the unicode table: https://unicode-table.com/en/blocks/
57 *   There is a lot of character ranges that defines a set of characters in a language, such as:
58 *   0000—007F Basic Latin 
59 *   0080—00FF Latin-1 Supplement 
60 *   0100—017F Latin Extended-A 
61 *   0180—024F Latin Extended-B 
62 *   Maybe it will prove more useful than having a default charset
63 */
64 class HipArsd_TTF_Font : HipFont
65 {
66     import arsd.ttf;
67     protected float fontScale;
68     protected TtfFont font;
69     string path;
70     protected uint fontSize = 32;
71     protected uint _textureWidth, _textureHeight;
72     protected Hip_TTF_Font mainInstance;
73     protected Hip_TTF_Font[] clones;
74 
75     this(string path, uint fontSize = 32)
76     {
77         this.path = path;
78         this.fontSize = fontSize;
79     }
80     /**
81     *   This will cause a full load of the .ttf file, image generation and GPU upload. Should only be used
82     *   If you don't care about async
83     */
84     bool loadFromMemory(in ubyte[] data)
85     {
86         if(data == null || data.length == 0)
87             return false;
88         try{font = TtfFont(data);}
89         catch(Exception e){return false;}
90         return loadTexture(
91             generateImage(fontSize, _textureWidth, _textureHeight)
92         );
93     }
94 
95     bool partialLoad(in ubyte[] data, out ubyte[] rawImage)
96     {
97         font = TtfFont(data);
98         rawImage = generateImage(fontSize, _textureWidth, _textureHeight);
99         return true;
100     }
101 
102     override int getKerning(const(HipFontChar)* current, const(HipFontChar)* next) const
103     {
104         return cast(int)(fontScale*stbtt_GetGlyphKernAdvance(cast(stbtt_fontinfo*)&font.font, current.glyphIndex, next.glyphIndex));
105     }
106     override int getKerning(dchar current, dchar next) const
107     {
108         return cast(int)(fontScale*stbtt_GetCodepointKernAdvance(cast(stbtt_fontinfo*)&font.font, int(current), int(next)));
109     }
110 
111 
112     bool loadTexture(ubyte[] rawImage)
113     {
114         assert(rawImage !is null, "Must first generate a texture before uploading to GPU");
115         import hip.image;
116         import hip.hiprenderer.texture;
117         import hip.error.handler;
118         HipImageImpl img = new HipImageImpl();
119         img.loadRaw(rawImage, _textureWidth, _textureHeight, 1);
120         HipTexture t = new HipTexture();
121 
122         bool ret = t.load(img);
123         ErrorHandler.assertErrorMessage(ret, "Loading TTF", "Could not create texture for TTF");
124         texture = t;
125         return ret;
126     }
127 
128     /**
129     * This function returns a new font using the same data file, with a new size.
130     * The font data will reference to this same one 
131     */
132     override HipFont getFontWithSize(uint size)
133     {
134         Hip_TTF_Font ret = new Hip_TTF_Font(this.path, size);
135         ret.font = cast(TtfFont)this.font;
136         ret.mainInstance = cast(Hip_TTF_Font)(mainInstance is null ? this : mainInstance);
137         if(mainInstance)
138             mainInstance.clones~= ret;
139         else
140             clones~= ret;
141         
142         if(!ret.loadTexture(ret.generateImage(size, ret._textureWidth, ret._textureHeight)))
143             return null;
144 
145         return cast(HipFont)ret;
146     }
147 
148     protected RenderizedChar renderCharacter(dchar ch, int size, float shift_x = 0.0, float shift_y = 0.0)
149     {
150         RenderizedChar rch;
151         rch.ch = ch;
152         rch.data = font.renderCharacter(ch, size, rch.width, rch.height, shift_x, shift_y);
153         return rch;
154     }
155     /**
156     *   I'm no good packer. The image will be at least 2048xMinPowOf2
157     */
158     protected ubyte[] generateImage(int size, out uint width, out uint height, dstring charset = defaultCharset)
159     {
160         if(charset.length == 0)
161             return null;
162         scope RenderizedChar[] fontChars = new RenderizedChar[charset.length]; //TODO: USe that as it is more optimised
163         scope(exit)
164         {
165             foreach(ch; fontChars)
166                 ch.dispose();
167             import core.memory;
168             GC.free(fontChars.ptr);
169         }
170 
171         uint avgWidth = 0;
172         uint avgHeight = 0;
173         size_t i = 0;
174         foreach(dc; charset)
175         {
176             RenderizedChar rc = renderCharacter(dc, size);
177             avgWidth+= rc.width;
178             avgHeight+= rc.height;
179             fontChars[i++] = rc;
180         }
181         //Add as an error (pixel bleeding)
182         avgWidth = cast(uint)(avgWidth / charset.length) + 2;
183         avgHeight = cast(uint)(avgHeight / charset.length) + 2;
184         import std.algorithm.sorting:sort;
185         enum hSpacing = 1;
186         enum vSpacing = 1;
187         float x = 1;
188         float y = 0;
189         float optY = 0;
190         float scale = stbtt_ScaleForPixelHeight(&font.font, size);
191 
192         //Setting details
193         fontScale = scale;
194         
195         int ascent, descent, lineGap;
196         stbtt_GetFontVMetrics(&font.font, &ascent, &descent, &lineGap);
197 
198         lineBreakHeight = cast(uint)(int(ascent - descent + lineGap) * scale);
199 
200         //First guarantee the big size
201         import core.math:sqrt;
202         uint sqrtOfCharset = cast(uint)sqrt(cast(float)charset.length) + 1;
203         uint imageWidth = avgWidth * sqrtOfCharset;
204         uint imageHeight = avgHeight * sqrtOfCharset;
205         imageHeight = nextPowerOfTwo(imageHeight);
206         imageWidth = nextPowerOfTwo(imageWidth);
207 
208         width = imageWidth;
209         height = imageHeight;
210 
211         ubyte[] image = new ubyte[](imageWidth*imageHeight);
212 
213         int largestHeightInRow = 0;
214         foreach(fontCh; sort!"a.height > b.height"(fontChars))
215         {
216             int g = stbtt_FindGlyphIndex(&font.font, fontCh.ch);
217             int xAdvance, xOffset, yOffset, lsb;
218             int x1, y1;
219             stbtt_GetGlyphHMetrics(&font.font, g, &xAdvance, &lsb);
220             stbtt_GetGlyphBitmapBox(&font.font, g, scale,scale, &xOffset,&yOffset,&x1,&y1);
221             if(fontCh.ch == ' ')
222             {
223                 int space_x0, space_x1;
224                 if(fontCh.width == 0)
225                 {
226                     stbtt_GetCodepointBitmapBox(&font.font, int('n'), scale,scale, &space_x0, null, &space_x1, null);
227                     spaceWidth = space_x1 - space_x0;
228                 }
229                 else
230                     spaceWidth = fontCh.width;
231             }
232 
233             if(x + fontCh.width + hSpacing > imageWidth)
234             {
235                 x = hSpacing;
236                 y+= largestHeightInRow + vSpacing;
237                 largestHeightInRow = 0;
238             }
239 
240 
241             characters[fontCh.ch] = HipFontChar(fontCh.ch, cast(int)x, cast(int)y, fontCh.width, fontCh.height, 
242 
243                 xOffset, yOffset, round(xAdvance*scale), 0, 0,
244                 cast(float)x/imageWidth, cast(float)y/imageHeight,
245                 cast(float)fontCh.width/imageWidth, cast(float)fontCh.height/imageHeight, 
246                 g
247             );
248             fontCh.blitToImage(image, cast(int)(x), cast(int)(y), imageWidth, imageHeight);
249             x+= fontCh.width + hSpacing;
250 
251             if(fontCh.height > largestHeightInRow)
252                 largestHeightInRow = fontCh.height;
253         }
254         return image;
255     }
256 
257 }
258 
259 version(HipNullFont)
260     alias Hip_TTF_Font = HipNullFont;
261 else version(HipArsdFont)
262     alias Hip_TTF_Font = HipArsd_TTF_Font;
263 
264 
265 private struct RenderizedChar
266 {
267     dchar ch;
268     int size;
269     int width;
270     int height;
271 
272     ubyte[] data;
273 
274     void blitToImage(ref ubyte[] texture, int startX, int startY, int textureWidth, int textureHeight)
275     {
276         assert(startX + width < textureWidth, "Out of X boundaries");
277         for(size_t i = 0; i < height; i++)
278         {
279             size_t pos = (startY+i)*textureWidth + startX;
280             assert(startY + i < textureHeight, "Out of Y boundaries");
281             texture[pos..pos+width] = data[i*width..(i+1)*width];
282         }
283     }
284 
285     void dispose()
286     {
287         version(HipArsdFont)
288         {
289             if(data.ptr != null)
290             {
291                 import arsd.ttf;
292                 stbtt_FreeBitmap(data.ptr, null);
293             }
294         }
295         data = null;
296     }
297 }